# 剑指Offer题解 - Day70
# 剑指 Offer 43. 1~n 整数中 1 出现的次数
输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
示例 1:
输入:n = 12
输出:5
1
2
2
示例 2:
输入:n = 13
输出:6
1
2
2
限制:
1 <= n < 2^31
分析:
首先考虑暴力法解题。浅显易懂的方式就是遍历1~n的整数,然后分别计算每个数字中1出现的次数。由于n最大可达2^31
指数级,因此直接循环是不可接受的。
此时就需要寻找规律。首先约定俗成以下规则:
- 将当前位的数字记为
cur
。 - 将低于当前位的数字记为
low
。 - 将高于当前位的数字记为
high
。 - 将当前所处的位记为
digit
,也就是个位,十位,百位等。
搞清楚几个概念后,我们需要处理两件事情。
- 如何根据当前位的数字来计算此位出现1的可能性;
- 如何从当前位递推到更高位,从而继续计算当前位出现1的可能性。
先来看第一个问题,根据当前位数字的不同,分为以下三种情况:
cur === 0
:当前位数字为0,此位出现1的情况由高位决定,也就是high * digit
,因为高位的数字每变动一次,当前位就会产生1。cur === 1
:当前位数字为1,此位出现1的情况由高位和低位同时决定,也就是high * digit + low + 1
,高位不必多说,跟情况1是一样的。添加低位是因为低位不同数字的排列组合也是不同的数字,每个数字都会出现1。由于低位没有计入0,00,诸如此类,因此还需要再加1。cur === 2,3,...,9
:当前位数字为2~9,此位出现1的情况由高位决定,也就是(high + 1) * digit
,为什么要高位加1呢?因为多出来的一次来自于高位是high * digit
,当前位是1的情况。
再来看第二个问题,如何从当前位递推到更高位。此时需要以下四个步骤:
low += cur * digit
:将 cur 加入 low ,组成下轮 lowcur = high % 10
:下轮 cur 是本轮 high 的最低位high = Math.floor(high / 10)
:将本轮 high 最低位删除,得到下轮 highdigit *= 10
:所在位每轮 × 10- 递推终止条件是:当 high 和 cur 同时为 0 时,说明已经越过最高位,此时终止递推。
基于此,可以写出如下的最终代码:
/**
* @param {number} n
* @return {number}
*/
var countDigitOne = function(n) {
let digit = 1;
let res = 0;
let high = Math.floor(n / 10);
let cur = n % 10;
let low = 0;
while(high !== 0 || cur !== 0) {
if (cur === 0) res += high * digit;
else if (cur === 1) res += high * digit + low + 1;
else res += (high + 1) * digit;
low += cur * digit;
cur = high % 10;
high = Math.floor(high / 10);
digit *= 10;
}
return res;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 时间复杂度 O(logn)。
- 空间复杂度 O(1)。
# 总结
本题考查数学规律。难度系数困难。核心难点在于如何基于当前位的数字来推断出现1的情况;以及如何递推到更高位。
复杂度方面,循环次数为数字 n 的位数,因此时间复杂度是O(logn)
;维护额外的常数级别的变量占用O(1)
的空间。